Repository navigation
feat(lint)!: refuse an object that shows two visible fields under one label - #33639
objectstack-fleet[bot] wants to merge 7 commits into
Conversation
New gating rule field-label-duplicate (lintDuplicateFieldLabels): compares each object's visible fields, the platform-injected columns included, as translateObject serves them in the authored pass and in every bundle or built-in system-label locale. Runs on validate, build, lint and the object publish door. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5mdmzDcF58D1JdRH4y
…in os explain Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5mdmzDcF58D1JdRH4y
…om the zh-CN owner column Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5mdmzDcF58D1JdRH4y
…ner column; register the ADR-0087 entry and changeset Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5mdmzDcF58D1JdRH4y
…ule now refuses Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5mdmzDcF58D1JdRH4y
📓 Docs Drift CheckThis PR changes 2 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 172 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7f5bc4b022981672cd39cdd784677422ddc6491e && git checkout 7f5bc4b022981672cd39cdd784677422ddc6491e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3c0033f9d7e9ee49404db6390a7953eb4bb5152e 1e112d65fd7552e0d844732a20c2db77bbd93404 && git checkout -B drift-repro 3c0033f9d7e9ee49404db6390a7953eb4bb5152e && git merge --no-ff 1e112d65fd7552e0d844732a20c2db77bbd93404
node scripts/docs-audit/affected-docs.mjs --json 3c0033f9d7e9ee49404db6390a7953eb4bb5152e
|
|
domain:spec#4 · os-project-manager · session_01FEkQ5mecqk8M5q9vjKj6aS Contract reviewServed-tier: Inputs read: card #33545 body and all four comments (triage 6886885827, claim 6887573651, amendments 6887617666 and 6887847864, dev report 6888745802); parent #33506 body and all comments; PR #33639 body, its 16-file list and the net diff against ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentNarrowing against the published spec on The Attempts to break it, each read in the cited code:
Findings:
No must-fix. No false positive on legitimate metadata was found on the visibility, system-field or locale axes beyond the corners graded above. Implemented-by: VERDICT: PASS |
|
domain:spec#4 · os-project-manager · session_01FEkQ5mecqk8M5q9vjKj6aS Red read · head
|
|
domain:spec#4 · os-project-manager · session_01FEkQ5mecqk8M5q9vjKj6aS ACCEPT · PR #33639 (#33545) · head
|
Requested by Leehom · project thread
Fixes #33545
Part of #33506
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — the authoring half (an AI builder reusing
owner_idrather than adding a second owner field) is the skills lane's on the parent card;os explain field-label-duplicatecarries the rule's full text, and four CLI transcripts incontent/docsmove their author-time rule count from 67 to 68 (claim amendment 6887617666).Severity ruling: error at the author door, installed apps unaffected at load (claim 6887573651, Ruling-ref 「开放 p3卡认领」). Measured: no boot or registration path runs the author-time rules, so no fork.
维护者速读
@objectstack/lint新增一条门禁规则field-label-duplicate:同一对象上两个及以上可见字段(含平台注入的owner_id、创建/修改时间与人)在作者写的标签或任一语言下显示为同一个标签,就报错。os build/os validate/os lint和元数据发布门(对象写入)都执行它。示例应用里三处中文标签撞了owner_id的「负责人」,已改名(showcase 的任务「处理人」、团队「组长」,todo 的任务「处理人」),并登记了 ADR-0087 迁移条目。owner_id(中文「负责人」)和新加的assignee(也叫「负责人」),表格出两列「负责人」、看板卡片显示两次,用户分不清改的是哪个。构建时就拦下,比上线后再发现好。packages/lint/src/lint-duplicate-field-labels.ts(比较哪些字段、哪些语言),packages/lint/src/authoring-rules.ts(新登记项,门禁 + 对象发布门),examples/app-showcase/src/system/translations/index.ts(示例改名)。pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/lint-duplicate-field-labels.test.ts,预期 9 个用例全绿;其中第一个就是 staging 的owner_id+assignee「负责人」。What changes
packages/lint/src/lint-duplicate-field-labels.ts(new):lintDuplicateFieldLabels(stack), rule idFIELD_LABEL_DUPLICATE=field-label-duplicate,error. Per object it compares the fields the object SERVES: its own fields with everyobjectExtensionsfield of the stack folded in (objectsAsServed, tied fields left out, aslintAutonumberFormatsdoes), plus the columns the registry injects on it (injectedColumnDefsFor, the spec tablesapplySystemFieldsspreads; an authored field of the same name replaces the injected one).hidden: truefields are skipped; the injectedorganization_id,owning_business_unit_idandqueueare hidden by their own definitions, soowner_idand the four audit columns are compared. Labels are compared trimmed, once as authored and once per locale, through the spec's own read-exit translatortranslateObject(no second copy of its ladder). Locales: every locale atranslationsbundle of the stack carries plusBUILTIN_SYSTEM_FIELD_LABEL_LOCALES(en,zh-CN,ja-JP,es-ES), the locales a/metaread translates the injected columns into with no bundle at all. That second set is what catches the AI 搭建新增的「负责人」字段与系统字段 owner_id 同名,表格出现两列「负责人」、看板卡片重复显示 #33506 case: the author'sassigneekeeps 负责人 whileowner_idis served 负责人 to azh-CNreader. One finding per group of fields sharing a label; it names every field, the label, the locale(s) when only some clash, and the injected column(s) involved; the hint says rename,hidden: true, or drop the duplicate and use the platform column.packages/lint/src/authoring-rules.ts: one entry,tier: 'gating',input: 'parsed', all three commands,surfaces: CLI_AND_RUNTIME,runtimeTypes: ['object'](thelintSummaryRollupsshape).index.ts: barrel export of the function, the id constant and the finding type.packages/lint/src/rule-explanations.ts:os explain field-label-duplicateentry (additive, appended after the last entry and at the end of the table; in-flight PR fix(objectql, lint): synced objects are told to drop write verbs, not to open userActions #33517 also edits this file).*.user-text.tscatalogs hold sentences for door ADVISORIES only (lint-autonumber-formats.user-text.ts: "The two errors … refuse the write and have no sentence here"); this rule is an error.lint-duplicate-field-labels.test.ts(new, 9 cases: the AI 搭建新增的「负责人」字段与系统字段 owner_id 同名,表格出现两列「负责人」、看板卡片重复显示 #33506 pair red, hidden duplicate clean, distinct labels clean,ownership: 'none'clean, a clash only one translated locale creates naming that locale, an authored clash named once with no locale, an authoredowner_idwith its own label replacing the injected one, the registry entry on all three commands, and the explanation naming exactly the built-in locales);runtime-gate.object-writes.test.ts(the door roster andrulesRunpins gain the entry; a new describe: the write that adds the clash is refused with a name-keyed path, the hidden and relabelled controls publish, a sibling write is not charged with a stored object's clash, and a re-save of the clashing object is judged on its body).example-showcaseshowcase_task.assignee负责人 → 处理人 andshowcase_team.lead负责人 → 组长 (plus thep_assigneeaction-param label and its comment, which said it matchesshowcase_task.assignee);example-todotodo_task.owner负责人 → 处理人 (and thetodo_tasks_by_ownerreport label/description that names the same field). Each of the three was served 负责人 besideowner_idto azh-CNreader.packages/spec/src/migrations/entries/semantic/18.object-field-label-duplicate-refused.ts(a structured TODO: which field keeps the label is the author's choice, so no D2 conversion); the changeset carriesregistered object-field-label-duplicate-refused.protocol.runtime-authoring-gate.test.ts: four fixture objects declareowner: { type: 'text', label: 'Owner' }, which the door now reads as one label with the injectedowner_id("Owner"); relabelled'Owner Name'(lines 413, 703, 714, 791).protocol.unknown-picklist-binding-refusal.test.ts: theobject()helper gives every bound select field the label'Tier', so a fixture bindingtandtiershowed two "Tier" columns; the label is now`Tier (${field})`(line 187). Neither test is about labels orowner_id; 266 of 266 metadata-protocol tests green after the relabel. No other fixture in the swept suites or the dogfood battery was refused.Running author-time rules (67)→(68)(claim amendment 6887617666)..changeset/33545-duplicate-field-labels.md:@objectstack/lintand@objectstack/specminor(narrowed), BREAKING banner with FROM → TO lines and fixes.Where the rule runs, measured
os build/os validate/os lintandos verify's author-time step: the registry (authoringRulesFor). Scaffold validation runs it too (scaffold-validate.ts).saveMetaItemwithstate: 'active'and the draft→active promotion, throughjudgeRuntimeAuthoringRules/runtimeAuthoringRulesFor('object'). Studio, REST/metaand MCP/AI authors reach it there. The door snapshot carries notranslations, so at the door the authored labels and the built-in system-field labels are compared (which is the staging case).AUTHORING_RULES(git grepover every non-test consumer: the CLI commands, scaffold validate,os verify, the metadata-protocol save/publish gate;metadata-diagnostics.tsimports two named lint functions only, not the registry). So an installed app keeps loading — no fork.packages/; the builder is in cloud). What this repo offers it is the publish door above, which every MCP/REST/Studio object write passes; nothing was done in cloud.Tests
pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/lint-duplicate-field-labels.test.ts src/runtime-gate.object-writes.test.ts: 53 passed.if (!isRec(stack)) return findings;→ always return, viascripts/ablation-replace.mjs): 6 red, among them the AI 搭建新增的「负责人」字段与系统字段 owner_id 同名,表格出现两列「负责人」、看板卡片重复显示 #33506owner_id+assigneecase, the one-locale clash, the authored clash, the registry entry and both door cases; restored, blob equals HEAD,git diff HEADempty.if (def.hidden === true) continue;neutralised): 2 red, the hidden control in the rule test and the hidden/relabelled publish control at the door; restored the same way.Acceptance notes
packages/appsbuilt through the real CLI (os build): onlyexample-showcaseandexample-todowere refused, the three fields relabelled above; all 11 build clean now. HotCRM (5 packages, 23 objects, 5 bundles): 0 findings, with a positive control (a duplicate of each object's first visible label) firing on 23 of 23 objects, so the probe reached every object.os buildand the door snapshot carries no translations:sys_import_jobcreated_at/created_count"Создано" (ru),sys_oauth_resourceid/identifier"المعرّف" (ar),sys_useraccountable_user_id/manager_id"Responsable" (fr). Each is a reader-facing duplicate column in that locale. Left untouched here (translation pipeline output outside the card); named as a follow-up in the report.en/zh-CN/ja-JP/es-ESsystem-field labels, which is the staging case; a clash only a stored translation bundle creates is caught byos build/os validate/os lint, not at the object write.Verification
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat 274a4bd: 116 commands, every one run, every one exit 0. Three first exited 3 (prerequisite not met, not a finding) and were rerun after the dependency builds:check-plugin-teardown-shape.mjs --self-test(pinned commit fetched),spec check:skill-examples(client builds),check:dual-build-cjs-loads(dist builds). Log:/mnt/project-files/pm-orders/spec/33545-gates-ran.txt.@objectstack/lint(including its test typecheck),@objectstack/metadata-protocol,@objectstack/spec(tsc --noEmit),example-showcase,example-todo: exit 0.pnpm --filter @objectstack/dogfood testunder the verify lock, after building@objectstack/dogfood^...): command exit 0, 467 files passed and 3 skipped, 3950 tests passed and 42 skipped. No fixture was refused.dispatch-gatesnames as having no local invocation (Build Core, the Dogfood Regression Gate's merge-queue run, the type-check lanes over CI's whole-workspace filters).🤖 Generated with Claude Code
https://claude.ai/code/session_01FEkQ5mdmzDcF58D1JdRH4y
Generated by Claude Code